/*
 * Copyright 2012 Quytelda K. Gaiwin
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.tamalin.babel.engine;

import org.tamalin.babel.*;

import java.io.File;
import java.io.FileInputStream;
import java.util.Scanner;
import java.util.Hashtable;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Created: Aug 13, 2011
 * @author Quytelda Gaiwin
 */
public class SequenceGenerator
{
	/**
	* Creates a new SequenceGenerator object that obtains definitions from the file referred to by definitions.
	* @param definitions the file to read sequence definitions from
	*/
	public SequenceGenerator(File definitions)
	{
		// Initialize using the definintions file passed as the argument
		// If null was passed, use DEFAULT_DEFS.
		init((definitions != null) ? definitions : DEFAULT_DEFS);

		// Initialize the random generator
		random = new SecureRandom();
	}

	/**
	* This method is called to initialize a SequenceGenerator object.
	* It read and parses the contents of the provided definitions file.
	* @param definitions the file from which to read sequence definitions
	*/
	private void init(File definitions)
	{
		// check to ensure the definitions file really exists
		if(!definitions.exists())
		{
			// if the file does not exists, complain and exit
			Logger.getLogger(SequenceGenerator.class.getName()).log(Level.SEVERE,"Provided definitions file ({0}) doesn't exist!", definitions.getPath());
			System.exit(1);
		}

		// log the file reading event
		Logger.getLogger(SequenceGenerator.class.getName()).log(Level.INFO, "Reading definition file {0}", definitions.getPath());

		// attempt to read and parse each line of the definitions file
		try
		{
			// define a new scanner to read a FileInputStream from the definitions file
			Scanner in = new Scanner(new FileInputStream(definitions));

			while(in.hasNextLine())
			{
				// read the line
				String line = in.nextLine();

				// check that the line is not a comment
				// comments begin with # or ; (so ignore these)
				if(!(line.startsWith("#") || line.startsWith(";")))
				{
					// seperate the key and the group members at the first instance of the equals sign
					String key = line.substring(0, line.indexOf('='));
					String values = line.substring(line.indexOf('=') + 1, line.length());

					/* If the "file:[filename]" designation is used after the equals sign,
					 * the elements will be loaded from a separate file.
					 * Imported elements will be treated as strings corresponding to
					 * each line in the file; therefore the elements may contain more than one character.
					 */
					if(values.startsWith("file:"))
					{
						// Parse the file name.
						File list = new File(values.substring(values.indexOf(':') + 1, values.length()));
						
						// Check if the file exists
						// If not, complain and exit in strict mode.
						if(!list.exists()) 
						{
							Logger.getLogger(SequenceGenerator.class.getName()).log(Level.WARNING, "File {0} doesn't exist!", list.getPath());
							if(Main.isStrict()) System.exit(1);
						}
						
						Logger.getLogger(SequenceGenerator.class.getName()).log(Level.FINE, "Loading element list from {0}", list.getAbsolutePath());
						
						try
						{
							// Read the file line by line using java.util.Scanner
							FileInputStream fis = new FileInputStream(list);
							Scanner listIn = new Scanner(fis);
							
							// Append each line to a temporary ArrayList<String> as it is read,
							// since we can't be sure how many lines are in the file.
							ArrayList<String> tmp = new ArrayList<String>(1);
							while(listIn.hasNextLine())
							{
								tmp.add(listIn.nextLine());
							}
							
							// Move the collected elements into a static String[] array.
							String[] elements = new String[tmp.size()];
							elements = tmp.toArray(elements);
							
							// Add the element array to the Hashtable.
							// Use the key name as the identifier.
							stringVariables.put(key, elements);
						}
						catch(IOException ex)
						{
							ex.printStackTrace();
						}
						
						// move on the the next variable
						return;
					}
					
					/* At this point in execution, the variable declaration
					 * being parsed must contain a string of characters.
					 */
					 Logger.getLogger(SequenceGenerator.class.getName()).log(Level.FINE, "Adding variable "+ key + "=" + values);
					 char[] elements = values.toCharArray();
					 charVariables.put(key, elements);
				}
				
				
			}
		}
		catch(IOException ex)
		{
			Logger.getLogger(SequenceGenerator.class.getName()).log(Level.WARNING,"Unable to load definitions.", ex);
		}
	}

	/**
	* Returns a securely generated random integer less than <code>max</code>.
	* @param max the maximum desired return value
	* @return a securely generated random integer less than or equal to <code>max</code>
	*/
	private int randomInt(int max)
	{
		int r = Math.abs(random.nextInt() % max);
		return r;
	}

	/**
	* Generates a random sequence from the provided template.
	* @param template the template to use to generate the random sequence
	* @return the generated sequence matching <code>template</code>
	*/
	public String generate(String template)
	{
		// Split the template into it's components
		// The number of components in the template is the number of characters in the sequence.
		String[] keys = template.split(":");
		StringBuilder seq = new StringBuilder();
		for(int i = 0; i < keys.length; i++)
		{
			// Check for optional elements
			// If the key is in parenthesis, there is a 50% chance that the key will be included
			if(keys[i].startsWith("(") && keys[i].endsWith(")"))
				if(randomInt(1) == 1) continue;
				else keys[i] = keys[i].substring(1, keys[i].length() - 1);
				
			// Select a random element for the current template key
			
			//Try char variables first
			char[] c_elements = charVariables.get(keys[i]);
			String[] s_elements = stringVariables.get(keys[i]);
			if((c_elements  == null) != (s_elements == null))
			{	
				seq.append((c_elements != null) ? c_elements[randomInt(c_elements.length)] : s_elements[randomInt(s_elements.length)]);
			}
			else
			{
				// If the the key isn't in the Hashtable complain and exit in strict mode
				Logger.getLogger(SequenceGenerator.class.getName()).log(Level.WARNING, "Key {0} not recognized.", keys[i]);
				if(Main.isStrict()) System.exit(1);
			}
		}

		return seq.toString();
	}

	private SecureRandom random;
	private static final File DEFAULT_DEFS = new File(System.getProperty("java.class.path") + "/default.conf");
	private Hashtable<String, String[]> stringVariables = new Hashtable<String, String[]>();
	private Hashtable<String, char[]> charVariables = new Hashtable<String, char[]>();
}
